<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Plugins\Apple;

use Exception;
use Srv\Conf\ConfModel\AppleAuthConf;
use Srv\Libs\Common\CommFile;
use Srv\Libs\Common\CommJson;
use Srv\Libs\Common\CommNet;
use Srv\Libs\Common\CommString;
use Srv\Libs\Common\CommTime;
use Srv\Libs\ConfModel\NetworkConf;
use Srv\Libs\Frame\Conf;
use Srv\Libs\Frame\ControllerAbstract;
use Srv\Libs\Plugins\Jwt\JWT;
use Srv\Libs\Storage\DataRedis;

final class AppleStoreServer
{
    private string $appleBundleId                   = '';
    private string $appId                           = '';
    private string $authIss                         = '';
    private string $authKeyId                       = '';
    private string $authKeyFile                     = '';
    private string $rootCaCerFile                   = '';
    private string $appStoreServerUrl               = 'https://api.storekit.itunes.apple.com';
    private bool $isSandbox                         = false;
    private ?DataRedis $CcAuto                      = null;             // CcAuto
    private const APPLE_SERVER_PREFIX               = 'AP_SERVER_';     // 缓存令牌前缀

    /**
     * @param AppleAuthConf $AppleAuthConf
     * __construct
     */
    public function __construct(AppleAuthConf $AppleAuthConf, bool $isSandbox)
    {
        $this->appleBundleId        = $AppleAuthConf->getAppleBundleId();
        $this->appId                = $AppleAuthConf->getAppId();
        $this->authIss              = $AppleAuthConf->getIssId();
        $this->authKeyId            = $AppleAuthConf->getKeyId();
        $this->authKeyFile          = $AppleAuthConf->getAuthKeyFile();
        $this->rootCaCerFile        = $AppleAuthConf->getRootCaCerFile();
        $this->isSandbox            = $isSandbox;
        if($this->isSandbox) $this->appStoreServerUrl = 'https://api.storekit-sandbox.itunes.apple.com';
        $this->CcAuto               = ControllerAbstract::getCacheAuto();
    }

    /**
     * @return bool
     * sendTestNotifications
     */
    public function sendTestNotifications():bool
    {
        // https://developer.apple.com/documentation/appstoreserverapi/request_a_test_notification
        $url            = $this->appStoreServerUrl.'/inApps/v1/notifications/test';
        $jwtToken       = $this->getJwtToken();
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'POST', 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo        = CommNet::getUrlInfo($url, $param);
            $httpCode       = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) break;
            if($httpCode === 200) return true;
        }while(--$tryNum >= 0);
        return false;
    }

    /**
     * @param string $transactionId
     * @param string $transactionBody
     * @return array
     * getTransactionInfo
     */
    public function getTransactionInfo(string $transactionId, string &$transactionBody):array
    {
        // https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info
        $tplData        = ['TRANSACTION_ID' => $transactionId];
        $url            = CommString::replaceTplStr($this->appStoreServerUrl.'/inApps/v1/transactions/{TRANSACTION_ID}', $tplData);
        $jwtToken       = $this->getJwtToken();
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'GET', 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo                = CommNet::getUrlInfo($url, $param);
            $httpCode               = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) break;
            if($httpCode === 200){
                $transactionBody    = $urlInfo['body']??'';
                $bodyData           = CommJson::decodeArray(trim($transactionBody));
                if(isset($bodyData['signedTransactionInfo']) && is_string($bodyData['signedTransactionInfo'])){
                    $payLoadDst             = [];
                    if($this->deSignedPayload($bodyData['signedTransactionInfo'], $payLoadDst)){
                        $transactionBody    = CommJson::encodeArray(array_merge($bodyData, ['_payLoadDst' => $payLoadDst]));
                        return $payLoadDst;
                    }
                }
                break;
            }
        }while(--$tryNum >= 0);
        return [];
    }

    /**
     * @param string $transactionId
     * @return array
     * getTransactionHistory
     */
    public function getTransactionHistory(string $transactionId):array
    {
        // https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history
        $tplData        = ['TRANSACTION_ID' => $transactionId];
        $url            = CommString::replaceTplStr($this->appStoreServerUrl.'/inApps/v1/history/{TRANSACTION_ID}', $tplData);
        $jwtToken       = $this->getJwtToken();
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'GET', 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo        = CommNet::getUrlInfo($url, $param);
            $httpCode       = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) break;
            if($httpCode === 200){
                $bodyData   = CommJson::decodeArray(trim($urlInfo['body']??''));
                if(isset($bodyData['bundleId'])) return $bodyData;
                break;
            }
        }while(--$tryNum >= 0);
        return [];
    }

    /**
     * @param string $transactionId
     * @param string $uuId
     * @param int $regTime
     * @param int $rechargeTotal
     * @param int $balance
     * @return bool
     * sendConsumption
     */
    public function sendConsumption(string $transactionId, string $uuId, int $regTime, int $rechargeTotal, int $balance):bool
    {
        // https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history
        $tplData        = ['TRANSACTION_ID' => $transactionId];
        $url            = CommString::replaceTplStr($this->appStoreServerUrl.'/inApps/v1/transactions/consumption/{TRANSACTION_ID}', $tplData);
        $currTime       = CommTime::getTimeStamp();
        // 账号注册时长[0-未申明,1-(0-3d),2-(3-10d),3-(10-30d),4-(30-90d),5-(90-180d),6-(180-365d),7-(>365d)]
        $regDiffTime    = intval(ceil(($currTime - $regTime)/86400));
        if($regDiffTime < 0){          $accountTenure  = 0;
        }else if($regDiffTime <= 3){   $accountTenure  = 1;
        }else if($regDiffTime <= 10){  $accountTenure  = 2;
        }else if($regDiffTime <= 30){  $accountTenure  = 3;
        }else if($regDiffTime <= 90){  $accountTenure  = 4;
        }else if($regDiffTime <= 180){ $accountTenure  = 5;
        }else if($regDiffTime <= 365){ $accountTenure  = 6;
        }else{                         $accountTenure  = 7; }
        // 购买金额[0-未公布,1-(0$),2-(0.01-49.99$),3-(50-99.99$),4-(100-499.99$),5-(500-999.99$),6-(1000-1999.99$),7-(>2000$)]
        if($rechargeTotal < 0){             $totalMoneyVal  = 0;
        }else if($rechargeTotal <= 0){      $totalMoneyVal  = 1;
        }else if($rechargeTotal <= 4999){   $totalMoneyVal  = 2;
        }else if($rechargeTotal <= 9999){   $totalMoneyVal  = 3;
        }else if($rechargeTotal <= 49999){  $totalMoneyVal  = 4;
        }else if($rechargeTotal <= 99999){  $totalMoneyVal  = 5;
        }else if($rechargeTotal <= 199999){ $totalMoneyVal  = 6;
        }else{                              $totalMoneyVal  = 7; }
        // 消费状态[0-未公布,1-不会被消耗,2-已部分消耗,3-已完全消耗]
        if($balance === -1){        $useStatus  = 0;
        }else if($balance === -2){  $useStatus  = 1;
        }else if($balance === 0){   $useStatus  = 3;
        }else{                      $useStatus  = 2; }
        $payload        = [
            'accountTenure'             => $accountTenure,  // 账号注册时长[0-未申明,1-(0-3d),2-(3-10d),3-(10-30d),4-(30-90d),5-(90-180d),6-(180-365d),7-(>365d)]
            'appAccountToken'           => $uuId,           // 下单时APP的UUID
            'consumptionStatus'         => $useStatus,      // 消费状态[0-未公布,1-不会被消耗,2-已部分消耗,3-已完全消耗]
            'customerConsented'         => true,            // 客户同意
            'deliveryStatus'            => 0,               // 运行正常
            'lifetimeDollarsPurchased'  => $totalMoneyVal,  // 购买金额[0-未公布,1-(0$),2-(0.01-49.99$),3-(50-99.99$),4-(100-499.99$),5-(500-999.99$),6-(1000-1999.99$),7-(>2000$)]
            'lifetimeDollarsRefunded'   => 0,               // 总退款金额[0-未公布,1-(0$),2-(0.01-49.99$),3-(50-99.99$),4-(100-499.99$),5-(500-999.99$),6-(1000-1999.99$),7-(>2000$)]
            'platform'                  => 1,               // 购买的平台[0-未申明,1-苹果,2-非苹果]
            'playTime'                  => 0,               // 使用应用时长[0-未公布,1-(0-5m),2-(5-60m),3-(1-6h),4-(6-24h),5-(1-4d),6-(4-16d),7-(>16d)]
            'sampleContentProvided'     => true,            // 是否提供了免费样本或试用版
            'userStatus'                => 1,               // 帐户状态[0-未申明,1-正常,2-被暂停,3-被终止,4-被有限的访问权限]
        ];
        $jwtToken       = $this->getJwtToken();
        $param          = ['TIMEOUT' => 10, 'METHOD' => 'PUT', 'POST_DATA' => CommJson::encode($payload), 'AUTH_TYPE' => 'Bearer', 'PASS' => $jwtToken, 'HEADER' => ['Content-Type' => 'application/json']];
        $NetworkConf    = Conf::getNetworkConfGroup()->getByGroupName('auto');
        if($NetworkConf instanceof NetworkConf) $param = array_merge($param, $NetworkConf->getProxyParam(true, true));
        $tryNum         = 2;
        do{
            $urlInfo        = CommNet::getUrlInfo($url, $param);
            $httpCode       = intval($urlInfo['http_code']??-1);
            if($httpCode >= 400 && $httpCode <= 599) break;
            if(in_array($httpCode, [202, 200], true)) return true;
        }while(--$tryNum >= 0);
        return false;
    }

    /**
     * @return string
     * getJwtToken
     */
    private function getJwtToken():string
    {
        // 生成 JWT https://developer.apple.com/documentation/appstoreserverapi/generating_tokens_for_api_requests
        $privateKey     = '';
        if(!CommFile::getContent($this->authKeyFile, $privateKey)) return '';
        $cacheKey       = self::APPLE_SERVER_PREFIX.md5($privateKey);
        $cacheStr       = $this->CcAuto->get($cacheKey);
        if(is_string($cacheStr) && strlen($cacheStr) > 32) return $cacheStr;
        $currTime       = CommTime::getTimeStamp();
        $expTime        = 1800;
        $payload        = [
            'iss'   => $this->authIss,
            'iat'   => $currTime - 60,
            'exp'   => $currTime + $expTime,    // 不能超过60分钟
            'aud'   => 'appstoreconnect-v1',
            'bid'   => $this->appleBundleId,
        ];
        try{
            $jwtToken   = JWT::encode($payload, $privateKey, 'ES256', $this->authKeyId, []);
            if(strlen($jwtToken) > 32) $this->CcAuto->set($cacheKey, $jwtToken, max($expTime - 60, 1));
        }catch (Exception $Exception){
            $jwtToken   = '';
        }
        return $jwtToken;
    }

    /**
     * @param string $signedPayloadStr
     * @param array $payLoadDst
     * @return bool
     * deSignedPayload
     */
    private function deSignedPayload(string $signedPayloadStr, array &$payLoadDst):bool
    {
        $signedPayloadData  = explode('.', $signedPayloadStr);
        if(count($signedPayloadData) !== 3) return false;
        $signedHeadData     = CommJson::decode(base64_decode($signedPayloadData[0]));
        $algorithm          = $signedHeadData['alg']??'';
        $x5cList            = $signedHeadData['x5c']??[];
        if(!is_string($algorithm) || !is_array($x5cList) || strlen($algorithm) < 1 || count($x5cList) < 1) return false;
        $certificates       = [];
        foreach ($x5cList as $x5cContent){
            $certificate        = openssl_x509_read('-----BEGIN CERTIFICATE-----'.PHP_EOL.chunk_split($x5cContent, 64, PHP_EOL).'-----END CERTIFICATE-----'.PHP_EOL);
            if($certificate === false) return false;
            $certificates[]     = $certificate;
        }
        // AppleRootCA-G3
        $rootCaCerStr       = '';
        if(!CommFile::getContent($this->rootCaCerFile, $rootCaCerStr)){
            return false;
        }
        $certificate        = openssl_x509_read('-----BEGIN CERTIFICATE-----'.PHP_EOL.chunk_split(base64_encode($rootCaCerStr), 64, PHP_EOL).'-----END CERTIFICATE-----'.PHP_EOL);
        if($certificate === false) return false;
        $certificates[]     = $certificate;
        // 递归验证证书链
        $certificateLast    = null;
        foreach($certificates as $certificate){
            if(!is_null($certificateLast) && openssl_x509_verify($certificateLast, $certificate) !== 1) return false;
            $certificateLast    = $certificate;
        }
        // 验证签名并解码
        $pkeyPublicObj      = openssl_pkey_get_public($certificates[0]);
        if($pkeyPublicObj === false) return false;
        $pkeyDetailData     = openssl_pkey_get_details($pkeyPublicObj);
        if($pkeyDetailData === false || !isset($pkeyDetailData['key']) || !is_string($pkeyDetailData['key']) || strlen($pkeyDetailData['key']) < 1) return false;
        try{
            $payLoadSrc     = [];
            $payLoadDst     = (array)JWT::decode($signedPayloadStr, $pkeyDetailData['key'], [$algorithm], $payLoadSrc);
            if(isset($payLoadDst['data']) && is_object($payLoadDst['data'])) $payLoadDst['data'] = (array)$payLoadDst['data'];
            return true;
        }catch(Exception $Exception){
            $payLoadDst     = [];
        }
        return false;
    }
}